﻿using System;
using System.IO;

namespace jbt.CodeCandy.AsTrEAM.StreamManager
{
    /// <summary>
    /// A stream what reports audit data, i.e. bytes read and written.
    /// </summary>
    public class AuditStream : Stream
    {
        #region public class AuditData

        /// <summary>
        /// Audit data provided by an <see cref="AuditStream"/> object.
        /// </summary>
        public class AuditData
        {
            /// <summary>
            /// Gets the number of bytes read from the audited stream.
            /// </summary>
            public long BytesRead
            {
                get;
                internal set;
            }

            /// <summary>
            /// Gets the number of bytes written to the audited stream.
            /// </summary>
            public long BytesWritten
            {
                get;
                internal set;
            }

            /// <summary>
            /// Gets the unique identifier of the audited stream.
            /// </summary>
            public string Id
            {
                get;
                internal set;
            }

            /// <summary>
            /// Gets a value indicating whether reading from the audited stream was started.
            /// </summary>
            /// <value>
            /// true if reading from the audited stream was started; otherwise, false.
            /// </value>
            public bool ReadStarted
            {
                get;
                internal set;
            }

            /// <summary>
            /// Gets a value indicating whether writing to the audited stream was started.
            /// </summary>
            /// <value>
            /// true if writing to the audited stream was started; otherwise, false.
            /// </value>
            public bool WriteStarted
            {
                get;
                internal set;
            }
        }

        #endregion public class AuditData

        private readonly AuditData _auditData;
        private readonly Action<AuditData>[] _auditActions;
        private readonly Stream _stream;

        /// <summary>
        /// Initializes a new instance of the <see cref="AuditStream"/> class with data.
        /// </summary>
        /// <param name="id">
        /// Unique identifier of the new object.
        /// </param>
        /// <param name="stream">
        /// Stream to be audited.
        /// </param>
        /// <param name="auditActions">
        /// Actions to be invoked when an audit event is available.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="stream"/> is null.
        /// </exception>
        public AuditStream(string id, Stream stream, params Action<AuditData>[] auditActions)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            _auditData = new AuditData();
            _auditData.Id = id;
            _auditActions = auditActions;
            _stream = stream;
        }

        /// <summary>
        /// Gets a value indicating whether the audited stream supports reading.
        /// </summary>
        /// <value>
        /// true if the stream supports reading; otherwise, false.
        /// </value>
        public override bool CanRead
        {
            get
            {
                return _stream.CanRead;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the audited stream supports seeking.
        /// </summary>
        /// <value>
        /// true if the stream supports seeking; otherwise, false.
        /// </value>
        public override bool CanSeek
        {
            get
            {
                return _stream.CanSeek;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the audited stream supports writing.
        /// </summary>
        /// <value>
        /// true if the stream supports writing; otherwise, false.
        /// </value>
        public override bool CanWrite
        {
            get
            {
                return _stream.CanWrite;
            }
        }

        /// <summary>
        /// Gets the length in bytes of the audited stream.
        /// </summary>
        /// <value>
        /// A long value representing the length of the stream in bytes.
        /// </value>
        /// <exception cref="NotSupportedException">
        /// The audited stream does not support seeking.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        /// Methods were called after the audited stream was closed.
        /// </exception>
        public override long Length
        {
            get
            {
                return _stream.Length;
            }
        }

        /// <summary>
        /// Gets or sets the position within the audited stream.
        /// </summary>
        /// <value>
        /// The current position within the stream.
        /// </value>
        /// <exception cref="IOException">
        /// An I/O error occurs.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// The auited stream does not support seeking.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        /// Methods were called after the audited stream was closed.
        /// </exception>
        public override long Position
        {
            get
            {
                return _stream.Position;
            }
            set
            {
                _stream.Position = value;
            }
        }

        /// <summary>
        /// Disposes the audited stream and invokes the audit listeners.
        /// </summary>
        /// <param name="disposing">
        /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
        /// </param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                DisposeDisposables();
            }

            base.Dispose(disposing);
        }

        private void DisposeDisposables()
        {
            foreach (var auditAction in _auditActions)
            {
                if (auditAction != null)
                {
                    try
                    {
                        auditAction(_auditData);
                    }
                    catch
                    {
                    }
                }
            }
        }

        /// <summary>
        /// Clears all buffers for the audited stream and causes any buffered data to be written to the underlying device.
        /// </summary>
        /// <exception cref="IOException">
        /// An I/O error occurs.
        /// </exception>
        public override void Flush()
        {
            _stream.Flush();
        }

        /// <summary>
        /// Reads a sequence of bytes from the audited stream and advances the position within the stream by the number of bytes read.
        /// </summary>
        /// <param name="buffer">
        /// An array of bytes. 
        /// When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.
        /// </param>
        /// <param name="offset">
        /// The zero-based byte offset in buffer at which to begin storing the data read from the audited stream.
        /// </param>
        /// <param name="count">
        /// The maximum number of bytes to be read from the audited stream.
        /// </param>
        /// <returns>
        /// The total number of bytes read into the buffer. 
        /// This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// The sum of offset and count is larger than the buffer length.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="buffer"/> is null.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="offset"/> or <paramref name="count"/> is negative.
        /// </exception>
        /// <exception cref="IOException">
        /// An I/O error occurs.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// The audited stream does not support reading.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        /// Methods were called after the audited stream was closed.
        /// </exception>
        public override int Read(byte[] buffer, int offset, int count)
        {
            _auditData.ReadStarted = true;

            var bytesRead = _stream.Read(buffer, offset, count);

            _auditData.BytesRead += bytesRead;

            return bytesRead;
        }

        /// <summary>
        /// Sets the position within the audited stream.
        /// </summary>
        /// <param name="offset">
        /// A byte offset relative to the origin parameter.
        /// </param>
        /// <param name="origin">
        /// A value of type <see cref="SeekOrigin"/> indicating the reference point used to obtain the new position.
        /// </param>
        /// <returns>
        /// The new position within the audited stream.
        /// </returns>
        /// <exception cref="IOException">
        /// An I/O error occurs.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// The audited stream does not support seeking, such as if the stream is constructed from a pipe or console output.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        /// Methods were called after the audited stream was closed.
        /// </exception>
        public override long Seek(long offset, SeekOrigin origin)
        {
            var newPosition = _stream.Seek(offset, origin);

            return newPosition;
        }

        /// <summary>
        /// Sets the length of the audited stream.
        /// </summary>
        /// <param name="value">
        /// The desired length of the audited stream in bytes.
        /// </param>
        /// <exception cref="IOException">
        /// An I/O error occurs.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// The audited stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        /// Methods were called after the audited stream was closed.
        /// </exception>
        public override void SetLength(long value)
        {
            _stream.SetLength(value);
        }

        /// <summary>
        /// Writes a sequence of bytes to the audited stream and advances the current position within audited stream by the number of bytes written.
        /// </summary>
        /// <param name="buffer">
        /// An array of bytes. 
        /// This method copies count bytes from buffer to the audited stream.
        /// </param>
        /// <param name="offset">
        /// The zero-based byte offset in buffer at which to begin copying bytes to the audited stream.
        /// </param>
        /// <param name="count">
        /// The number of bytes to be written to the audited stream.
        /// </param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            _auditData.WriteStarted = true;

            _stream.Write(buffer, offset, count);

            _auditData.BytesWritten += count;
        }
    }
}